1 /**
2    Compile-time reflection to find unit tests and set their properties.
3  */
4 module unit_threaded.reflection;
5 
6 import unit_threaded.from;
7 
8 /*
9    These standard library imports contain something important for the code below.
10    Unfortunately I don't know what they are so they're to prevent breakage.
11  */
12 import std.traits;
13 import std.algorithm;
14 import std.array;
15 
16 ///
17 alias void delegate() TestFunction;
18 
19 /**
20  * Common data for test functions and test classes
21  */
22 struct TestData {
23     string name;
24     TestFunction testFunction; ///only used for functions, null for classes
25     bool hidden;
26     bool shouldFail;
27     bool singleThreaded;
28     bool builtin;
29     string suffix; // append to end of getPath
30     string[] tags;
31     TypeInfo exceptionTypeInfo; // for ShouldFailWith
32     int flakyRetries = 0;
33 
34     /// The test's name
35     string getPath() const pure nothrow {
36         string path = name.dup;
37         import std.array : empty;
38 
39         if (!suffix.empty)
40             path ~= "." ~ suffix;
41         return path;
42     }
43 
44     /// If the test is a class
45     bool isTestClass() @safe const pure nothrow {
46         return testFunction is null;
47     }
48 }
49 
50 /**
51  * Finds all test cases (functions, classes, built-in unittest blocks)
52  * Template parameters are module strings
53  */
54 const(TestData)[] allTestData(MOD_STRINGS...)()
55         if (from!"std.meta".allSatisfy!(from!"std.traits".isSomeString, typeof(MOD_STRINGS))) {
56     import std.array : join;
57     import std.range : iota;
58     import std.format : format;
59     import std.algorithm : map;
60 
61     string getModulesString() {
62         string[] modules;
63         foreach (i, module_; MOD_STRINGS)
64             modules ~= "module%d = %s".format(i, module_);
65         return modules.join(", ");
66     }
67 
68     enum modulesString = getModulesString;
69     mixin("import " ~ modulesString ~ ";");
70     mixin("return allTestData!(" ~ MOD_STRINGS.length.iota.map!(i => "module%d".format(i))
71             .join(", ") ~ ");");
72 }
73 
74 /**
75  * Finds all test cases (functions, classes, built-in unittest blocks)
76  * Template parameters are module symbols
77  */
78 const(TestData)[] allTestData(MOD_SYMBOLS...)()
79         if (!from!"std.meta".anySatisfy!(from!"std.traits".isSomeString, typeof(MOD_SYMBOLS))) {
80     auto allTestsWithFunc(string expr)() pure {
81         import std.traits : ReturnType;
82         import std.meta : AliasSeq;
83 
84         //tests is whatever type expr returns
85         ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests;
86         foreach (module_; AliasSeq!MOD_SYMBOLS) {
87             tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_
88         }
89         return tests;
90     }
91 
92     return allTestsWithFunc!"moduleTestClasses"
93         ~ allTestsWithFunc!"moduleTestFunctions" ~ allTestsWithFunc!"moduleUnitTests";
94 }
95 
96 private template Identity(T...) if (T.length > 0) {
97     static if (__traits(compiles, { alias x = T[0]; }))
98         alias Identity = T[0];
99     else
100         enum Identity = T[0];
101 }
102 
103 /**
104  * Finds all built-in unittest blocks in the given module.
105  * Recurses into structs, classes, and unions of the module.
106  *
107  * @return An array of TestData structs
108  */
109 TestData[] moduleUnitTests(alias module_)() pure nothrow {
110 
111     // Return a name for a unittest block. If no @Name UDA is found a name is
112     // created automatically, else the UDA is used.
113     // the weird name for the first template parameter is so that it doesn't clash
114     // with a package name
115     string unittestName(alias _theUnitTest, int index)() @safe nothrow {
116         import std.conv : text, to;
117         import std.traits : fullyQualifiedName;
118         import std.traits : getUDAs;
119         import std.meta : Filter;
120         import unit_threaded.attrs : Name;
121 
122         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
123 
124         enum nameAttrs = getUDAs!(_theUnitTest, Name);
125         static assert(nameAttrs.length == 0 || nameAttrs.length == 1,
126                 "Found multiple Name UDAs on unittest");
127 
128         enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, _theUnitTest));
129         enum hasName = nameAttrs.length || strAttrs.length == 1;
130         enum prefix = fullyQualifiedName!(__traits(parent, _theUnitTest)) ~ ".";
131 
132         static if (hasName) {
133             static if (nameAttrs.length == 1)
134                 return prefix ~ nameAttrs[0].value;
135             else
136                 return prefix ~ strAttrs[0];
137         } else {
138             string name;
139             try {
140                 return prefix ~ "unittest" ~ (index).to!string;
141             } catch (Exception) {
142                 assert(false, text("Error converting ", index, " to string"));
143             }
144         }
145     }
146 
147     void function() getUDAFunction(alias composite, alias uda)() pure nothrow {
148         import std.traits : fullyQualifiedName, moduleName, isSomeFunction,
149             hasUDA;
150 
151         // Due to:
152         // https://issues.dlang.org/show_bug.cgi?id=17441
153         // moduleName!composite might fail, so we try to import that only if
154         // if compiles, then try again with fullyQualifiedName
155         enum moduleNameStr = `import ` ~ moduleName!composite ~ `;`;
156         enum fullyQualifiedStr = `import ` ~ fullyQualifiedName!composite ~ `;`;
157 
158         static if (__traits(compiles, mixin(moduleNameStr)))
159             mixin(moduleNameStr);
160         else static if (__traits(compiles, mixin(fullyQualifiedStr)))
161             mixin(fullyQualifiedStr);
162 
163         void function()[] ret;
164         foreach (memberStr; __traits(allMembers, composite)) {
165             static if (__traits(compiles, Identity!(__traits(getMember, composite, memberStr)))) {
166                 alias member = Identity!(__traits(getMember, composite, memberStr));
167                 static if (__traits(compiles, &member)) {
168                     static if (isSomeFunction!member && hasUDA!(member, uda)) {
169                         ret ~= &member;
170                     }
171                 }
172             }
173         }
174 
175         return ret.length ? ret[0] : null;
176     }
177 
178     TestData[] testData;
179 
180     void addMemberUnittests(alias composite)() pure nothrow {
181 
182         import unit_threaded.attrs;
183         import unit_threaded.uda : hasUtUDA;
184         import std.traits : hasUDA;
185         import std.meta : Filter, aliasSeqOf;
186         import std.algorithm : map, cartesianProduct;
187 
188         foreach (index, eLtEstO; __traits(getUnitTests, composite)) {
189 
190             enum dontTest = hasUDA!(eLtEstO, DontTest);
191 
192             static if (!dontTest) {
193 
194                 enum name = unittestName!(eLtEstO, index);
195                 enum hidden = hasUDA!(eLtEstO, HiddenTest);
196                 enum shouldFail = hasUDA!(eLtEstO, ShouldFail)
197                         || hasUtUDA!(eLtEstO, ShouldFailWith);
198                 enum singleThreaded = hasUDA!(eLtEstO, Serial);
199                 enum builtin = true;
200                 enum suffix = "";
201 
202                 // let's check for @Values UDAs, which are actually of type ValuesImpl
203                 enum isValues(alias T) = is(typeof(T)) && is(typeof(T) : ValuesImpl!U, U);
204                 alias valuesUDAs = Filter!(isValues, __traits(getAttributes, eLtEstO));
205 
206                 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags);
207                 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, eLtEstO)));
208                 enum exceptionTypeInfo = getExceptionTypeInfo!eLtEstO;
209                 enum flakyRetries = getFlakyRetries!(eLtEstO);
210 
211                 static if (valuesUDAs.length == 0) {
212                     testData ~= TestData(name, () {
213                         auto setup = getUDAFunction!(composite, Setup);
214                         auto shutdown = getUDAFunction!(composite, Shutdown);
215 
216                         if (setup)
217                             setup();
218                         scope (exit)
219                             if (shutdown)
220                                 shutdown();
221 
222                         eLtEstO();
223                     }, hidden, shouldFail, singleThreaded, builtin, suffix,
224                             tags, exceptionTypeInfo, flakyRetries);
225                 } else {
226                     import std.range;
227 
228                     // cartesianProduct doesn't work with only one range, so in the usual case
229                     // of only one @Values UDA, we bind to prod with a range of tuples, just
230                     // as returned by cartesianProduct.
231 
232                     static if (valuesUDAs.length == 1) {
233                         import std.typecons;
234 
235                         enum prod = valuesUDAs[0].values.map!(a => tuple(a));
236                     } else {
237                         mixin(`enum prod = cartesianProduct(` ~ valuesUDAs.length.iota.map!(
238                                 a => `valuesUDAs[` ~ guaranteedToString(a) ~ `].values`).join(
239                                 ", ") ~ `);`);
240                     }
241 
242                     foreach (comb; aliasSeqOf!prod) {
243                         enum valuesName = valuesName(comb);
244 
245                         static if (hasUDA!(eLtEstO, AutoTags))
246                             enum realTags = tags ~ valuesName.split(".").array;
247                         else
248                             enum realTags = tags;
249 
250                         testData ~= TestData(name ~ "." ~ valuesName, () {
251                             foreach (i; aliasSeqOf!(comb.length.iota))
252                                 ValueHolder!(typeof(comb[i])).values[i] = comb[i];
253                             eLtEstO();
254                         }, hidden, shouldFail, singleThreaded, builtin, suffix,
255                                 realTags, exceptionTypeInfo, flakyRetries);
256                     }
257                 }
258             }
259         }
260     }
261 
262     // Keeps track of mangled names of everything visited.
263     bool[string] visitedMembers;
264 
265     void addUnitTestsRecursively(alias composite)() pure nothrow {
266         import std.traits : fullyQualifiedName;
267 
268         mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
269 
270         if (composite.mangleof in visitedMembers)
271             return;
272         visitedMembers[composite.mangleof] = true;
273         addMemberUnittests!composite();
274         foreach (member; __traits(allMembers, composite)) {
275             enum notPrivate = __traits(compiles, mixin(member)); //only way I know to check if private
276             static if (notPrivate && // If visibility of the member is deprecated, the next line still returns true
277                     // and yet spills deprecation warning. If deprecation is turned into error,
278                     // all works as intended.
279                     __traits(compiles, __traits(getMember, composite,
280                         member)) && __traits(compiles, __traits(allMembers, __traits(getMember, composite, member)))
281                     && __traits(compiles, recurse!(__traits(getMember, composite, member)))) {
282                 recurse!(__traits(getMember, composite, member));
283             }
284         }
285     }
286 
287     void recurse(child)() pure nothrow {
288         enum notPrivate = __traits(compiles, child.init); //only way I know to check if private
289         static if (is(child == class) || is(child == struct) || is(child == union)) {
290             addUnitTestsRecursively!child;
291         }
292     }
293 
294     addUnitTestsRecursively!module_();
295     return testData;
296 }
297 
298 private TypeInfo getExceptionTypeInfo(alias Test)() {
299     import unit_threaded.uda : hasUtUDA, getUtUDAs;
300     import unit_threaded.attrs : ShouldFailWith;
301 
302     static if (hasUtUDA!(Test, ShouldFailWith)) {
303         alias uda = getUtUDAs!(Test, ShouldFailWith)[0];
304         return typeid(uda.Type);
305     } else
306         return null;
307 }
308 
309 private string valuesName(T)(T tuple) {
310     import std.range : iota;
311     import std.meta : aliasSeqOf;
312     import std.array : join;
313 
314     string[] parts;
315     foreach (a; aliasSeqOf!(tuple.length.iota))
316         parts ~= guaranteedToString(tuple[a]);
317     return parts.join(".");
318 }
319 
320 private string guaranteedToString(T)(T value) nothrow pure @safe {
321     import std.conv;
322 
323     try
324         return value.to!string;
325     catch (Exception ex)
326         assert(0, "Could not convert value to string");
327 }
328 
329 private string getValueAsString(T)(T value) nothrow pure @safe {
330     import std.conv;
331 
332     try
333         return value.to!string;
334     catch (Exception ex)
335         assert(0, "Could not convert value to string");
336 }
337 
338 private template isStringUDA(alias T) {
339     import std.traits : isSomeString;
340 
341     static if (__traits(compiles, isSomeString!(typeof(T))))
342         enum isStringUDA = isSomeString!(typeof(T));
343     else
344         enum isStringUDA = false;
345 }
346 
347 @safe pure unittest {
348     static assert(isStringUDA!"foo");
349     static assert(!isStringUDA!5);
350 }
351 
352 private template isPrivate(alias module_, string moduleMember) {
353     import unit_threaded.uda : HasTypes;
354 
355     alias ut_mmbr__ = Identity!(__traits(getMember, module_, moduleMember));
356 
357     static if (__traits(compiles, isSomeFunction!(ut_mmbr__))) {
358         static if (__traits(compiles, &ut_mmbr__))
359             enum isPrivate = false;
360         else static if (__traits(compiles, new ut_mmbr__))
361             enum isPrivate = false;
362         else static if (__traits(compiles, HasTypes!ut_mmbr__))
363             enum isPrivate = !HasTypes!ut_mmbr__;
364         else
365             enum isPrivate = true;
366     } else {
367         enum isPrivate = true;
368     }
369 }
370 
371 // if this member is a test function or class, given the predicate
372 private template PassesTestPred(alias module_, alias pred, string moduleMember) {
373     import std.traits : fullyQualifiedName;
374     import unit_threaded.meta : importMember;
375     import unit_threaded.uda : HasAttribute;
376     import unit_threaded.attrs : DontTest;
377 
378     //should be the line below instead but a compiler bug prevents it
379     //mixin(importMember!module_(moduleMember));
380     mixin("import " ~ fullyQualifiedName!module_ ~ ";");
381     alias I(T...) = T;
382     static if (!__traits(compiles, I!(__traits(getMember, module_, moduleMember)))) {
383         enum PassesTestPred = false;
384     } else {
385         alias member = I!(__traits(getMember, module_, moduleMember));
386 
387         template canCheckIfSomeFunction(T...) {
388             enum canCheckIfSomeFunction = T.length == 1 && __traits(compiles,
389                         isSomeFunction!(T[0]));
390         }
391 
392         private string funcCallMixin(alias T)() {
393             import std.conv : to;
394 
395             string[] args;
396             foreach (i, ParamType; Parameters!T) {
397                 args ~= `arg` ~ i.to!string;
398             }
399 
400             return moduleMember ~ `(` ~ args.join(`,`) ~ `);`;
401         }
402 
403         private string argsMixin(alias T)() {
404             import std.conv : to;
405 
406             string[] args;
407             foreach (i, ParamType; Parameters!T) {
408                 args ~= ParamType.stringof ~ ` arg` ~ i.to!string ~ `;`;
409             }
410 
411             return args.join("\n");
412         }
413 
414         template canCallMember() {
415             void _f() {
416                 mixin(argsMixin!member);
417                 mixin(funcCallMixin!member);
418             }
419         }
420 
421         template canInstantiate() {
422             void _f() {
423                 mixin(`auto _ = new ` ~ moduleMember ~ `;`);
424             }
425         }
426 
427         template isPrivate() {
428             static if (!canCheckIfSomeFunction!member) {
429                 enum isPrivate = !__traits(compiles, __traits(getMember, module_, moduleMember));
430             } else {
431                 static if (isSomeFunction!member) {
432                     enum isPrivate = !__traits(compiles, canCallMember!());
433                 } else static if (is(member)) {
434                     static if (isAggregateType!member)
435                         enum isPrivate = !__traits(compiles, canInstantiate!());
436                     else
437                         enum isPrivate = !__traits(compiles,
438                                     __traits(getMember, module_, moduleMember));
439                 } else {
440                     enum isPrivate = !__traits(compiles, __traits(getMember,
441                                 module_, moduleMember));
442                 }
443             }
444         }
445 
446         enum notPrivate = !isPrivate!();
447         enum PassesTestPred = !isPrivate!() && pred!(module_, moduleMember)
448                 && !HasAttribute!(module_, moduleMember, DontTest);
449     }
450 }
451 
452 /**
453  * Finds all test classes (classes implementing a test() function)
454  * in the given module
455  */
456 TestData[] moduleTestClasses(alias module_)() pure nothrow {
457 
458     template isTestClass(alias module_, string moduleMember) {
459         import unit_threaded.meta : importMember;
460         import unit_threaded.uda : HasAttribute;
461         import unit_threaded.attrs : UnitTest;
462         import std.traits : isAggregateType;
463 
464         alias member = Identity!(__traits(getMember, module_, moduleMember));
465 
466         static if (.isPrivate!(module_, moduleMember)) {
467             enum isTestClass = false;
468         } else static if (!__traits(compiles, isAggregateType!(member))) {
469             enum isTestClass = false;
470         } else static if (!isAggregateType!(member)) {
471             enum isTestClass = false;
472         } else static if (!__traits(compiles, { return new member; })) {
473             enum isTestClass = false; //can't new it, can't use it
474         } else {
475             enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest);
476             enum hasTestMethod = __traits(hasMember, member, "test");
477 
478             enum isTestClass = is(member == class) && (hasTestMethod || hasUnitTest);
479         }
480     }
481 
482     return moduleTestData!(module_, isTestClass, memberTestData);
483 }
484 
485 /**
486  * Finds all test functions in the given module.
487  * Returns an array of TestData structs
488  */
489 TestData[] moduleTestFunctions(alias module_)() pure {
490 
491     import unit_threaded.uda : isTypesAttr;
492 
493     template isTestFunction(alias module_, string moduleMember) {
494         import unit_threaded.meta : importMember;
495         import unit_threaded.attrs : UnitTest;
496         import unit_threaded.uda : HasAttribute, GetTypes;
497         import std.meta : AliasSeq;
498         import std.traits : isSomeFunction;
499 
500         alias member = Identity!(__traits(getMember, module_, moduleMember));
501 
502         static if (.isPrivate!(module_, moduleMember)) {
503             enum isTestFunction = false;
504         } else static if (AliasSeq!(member).length != 1) {
505             enum isTestFunction = false;
506         } else static if (isSomeFunction!member) {
507             enum isTestFunction = hasTestPrefix!(module_, moduleMember)
508                     || HasAttribute!(module_, moduleMember, UnitTest);
509         } else static if (__traits(compiles, __traits(getAttributes, member))) {
510             // in this case we handle the possibility of a template function with
511             // the @Types UDA attached to it
512             alias types = GetTypes!member;
513             enum isTestFunction = hasTestPrefix!(module_, moduleMember) && types.length > 0;
514         } else {
515             enum isTestFunction = false;
516         }
517 
518     }
519 
520     template hasTestPrefix(alias module_, string memberName) {
521         import std.uni : isUpper;
522         import unit_threaded.meta : importMember;
523 
524         alias member = Identity!(__traits(getMember, module_, memberName));
525 
526         enum prefix = "test";
527         enum minSize = prefix.length + 1;
528 
529         static if (memberName.length >= minSize
530                 && memberName[0 .. prefix.length] == prefix && isUpper(memberName[prefix.length])) {
531             enum hasTestPrefix = true;
532         } else {
533             enum hasTestPrefix = false;
534         }
535     }
536 
537     return moduleTestData!(module_, isTestFunction, createFuncTestData);
538 }
539 
540 private TestData[] createFuncTestData(alias module_, string moduleMember)() {
541     import unit_threaded.meta : importMember;
542     import unit_threaded.uda : GetAttributes, HasAttribute, GetTypes, HasTypes;
543     import unit_threaded.attrs;
544     import std.meta : aliasSeqOf;
545 
546     mixin(importMember!module_(moduleMember));
547     /*
548       Get all the test functions for this module member. There might be more than one
549       when using parametrized unit tests.
550 
551       Examples:
552       ------
553       void testFoo() {} // -> the array contains one element, testFoo
554       @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value
555       @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type
556       ------
557     */
558     // if the predicate returned true (which is always the case here), then it's either
559     // a regular function or a templated one. If regular we can get a pointer to it
560     enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember));
561 
562     static if (isRegularFunction) {
563 
564         enum func = &__traits(getMember, module_, moduleMember);
565         enum arity = arity!func;
566 
567         static if (arity == 0) // the reason we're creating a lambda to call the function is that test functions
568             // are ordinary functions, but we're storing delegates
569             return [memberTestData!(module_, moduleMember)(() { func(); })]; //simple case, just call the function
570         else {
571 
572             // the function has parameters, check if it has UDAs for value parameters to be passed to it
573             alias params = Parameters!func;
574 
575             import std.range : iota;
576             import std.algorithm : any;
577             import std.typecons : tuple, Tuple;
578 
579             bool hasAttributesForAllParams() {
580                 auto ret = true;
581                 foreach (p; params) {
582                     if (tuple(GetAttributes!(module_, moduleMember, p)).length == 0) {
583                         ret = false;
584                     }
585                 }
586                 return ret;
587             }
588 
589             static if (!hasAttributesForAllParams) {
590                 import std.conv : text;
591 
592                 pragma(msg, text("Warning: ", moduleMember,
593                         " passes the criteria for a value-parameterized test function",
594                         " but doesn't have the appropriate value UDAs.\n",
595                         "         Consider changing its name or annotating it with @DontTest"));
596                 return [];
597             } else {
598 
599                 static if (arity == 1) {
600                     // bind a range of tuples to prod just as cartesianProduct returns
601                     enum prod = [GetAttributes!(module_, moduleMember, params[0])].map!(
602                                 a => tuple(a));
603                 } else {
604                     import std.conv : text;
605 
606                     mixin(`enum prod = cartesianProduct(` ~ params.length.iota.map!(
607                             a => `[GetAttributes!(module_, moduleMember, params[` ~ guaranteedToString(a) ~ `])]`)
608                             .join(", ") ~ `);`);
609                 }
610 
611                 TestData[] testData;
612                 foreach (comb; aliasSeqOf!prod) {
613                     enum valuesName = valuesName(comb);
614 
615                     static if (HasAttribute!(module_, moduleMember, AutoTags))
616                         enum extraTags = valuesName.split(".").array;
617                     else
618                         enum string[] extraTags = [];
619 
620                     testData ~= memberTestData!(module_, moduleMember, extraTags)( // func(value0, value1, ...)
621                             () { func(comb.expand); }, valuesName);
622                 }
623 
624                 return testData;
625             }
626         }
627     } else static if (HasTypes!(mixin(moduleMember))) { //template function with @Types
628         alias types = GetTypes!(mixin(moduleMember));
629         TestData[] testData;
630         foreach (type; types) {
631 
632             static if (HasAttribute!(module_, moduleMember, AutoTags))
633                 enum extraTags = [type.stringof];
634             else
635                 enum string[] extraTags = [];
636 
637             alias member = Identity!(mixin(moduleMember));
638 
639             testData ~= memberTestData!(module_, moduleMember, extraTags)(() {
640                 member!type();
641             }, type.stringof);
642         }
643         return testData;
644     } else {
645         return [];
646     }
647 }
648 
649 // this funtion returns TestData for either classes or test functions
650 // built-in unittest modules are handled by moduleUnitTests
651 // pred determines what qualifies as a test
652 // createTestData must return TestData[]
653 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure {
654     import std.traits : fullyQualifiedName;
655 
656     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
657 
658     TestData[] testData;
659 
660     foreach (moduleMember; __traits(allMembers, module_)) {
661 
662         static if (PassesTestPred!(module_, pred, moduleMember))
663             testData ~= createTestData!(module_, moduleMember);
664     }
665 
666     return testData;
667 
668 }
669 
670 // TestData for a member of a module (either a test function or test class)
671 private TestData memberTestData(alias module_, string moduleMember, string[] extraTags = [])(
672         TestFunction testFunction = null, string suffix = "") {
673 
674     import unit_threaded.uda : HasAttribute, GetAttributes, hasUtUDA;
675     import unit_threaded.attrs;
676     import std.traits : fullyQualifiedName;
677 
678     mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible
679 
680     immutable singleThreaded = HasAttribute!(module_, moduleMember, Serial);
681     enum builtin = false;
682     enum tags = tagsFromAttrs!(GetAttributes!(module_, moduleMember, Tags));
683     enum exceptionTypeInfo = getExceptionTypeInfo!(mixin(moduleMember));
684     enum shouldFail = HasAttribute!(module_, moduleMember, ShouldFail)
685             || hasUtUDA!(mixin(moduleMember), ShouldFailWith);
686     enum flakyRetries = getFlakyRetries!(mixin(moduleMember));
687 
688     return TestData(fullyQualifiedName!module_ ~ "." ~ moduleMember, testFunction, HasAttribute!(module_, moduleMember,
689             HiddenTest), shouldFail, singleThreaded, builtin, suffix,
690             tags ~ extraTags, exceptionTypeInfo, flakyRetries);
691 }
692 
693 private int getFlakyRetries(alias test)() {
694     import unit_threaded.attrs : Flaky;
695     import std.traits : getUDAs;
696     import std.conv : text;
697 
698     alias flakies = getUDAs!(test, Flaky);
699 
700     static assert(flakies.length == 0 || flakies.length == 1, text("Only 1 @Flaky allowed, found ",
701             flakies.length, " on ", __traits(identifier, test)));
702 
703     static if (flakies.length == 1) {
704         static if (is(flakies[0]))
705             return Flaky.defaultRetries;
706         else
707             return flakies[0].retries;
708     } else
709         return 0;
710 }
711 
712 string[] tagsFromAttrs(T...)() {
713     static assert(T.length <= 1, "@Tags can only be applied once");
714     static if (T.length)
715         return T[0].values;
716     else
717         return [];
718 }